﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="LoadBalancer.cs" company="SoftServe">
//   071C#
// </copyright>
// <summary>
//   Logic for Load Balancer
// </summary>
// --------------------------------------------------------------------------------------------------------------------

using WebServer.BusinessLogic.Communication;

namespace WebServer.BusinessLogic
{
    using System;
    using System.Globalization;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using System.Threading;
    using WebServer.BusinessLogic.Logs;
    using System.Net;

    /// <summary>
    ///   TODO: Update summary.
    /// </summary>
    public class LoadBalancer
    {
        #region Fields

        /// <summary>
        /// Indicates if answering thread is running
        /// </summary>
        private bool isAnswering;

        /// <summary>
        /// Indicates if asking thread is running
        /// </summary>
        public bool IsAsking { get; private set; }

        /// <summary>
        /// Communicator reference that should be used while in LoadBalancer routines
        /// </summary>
        private Communicator _communicator;

        /// <summary>
        /// Constructor for LoadBalancer
        /// </summary>
        /// <param name="communicator"></param>
        public LoadBalancer(Communicator communicator)
        {
            this._communicator = communicator;
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the answering thread.
        /// </summary>
        public Thread AnsweringThread { get; set; }

        /// <summary>
        /// Gets or sets the asking thread.
        /// </summary>
        public Thread AskingThread { get; set; }

        /// <summary>
        ///   Gets or sets the least loaded server for load balancer.
        /// </summary>
        public string LeastLoadedServer { get; set; }

        #endregion

        #region Public Methods and Operators

        /// <summary>
        /// Get server with minimal load.
        /// </summary>
        /// <returns>
        /// Minimal loaded server.
        /// </returns>
        public string GetServerWithMinLoad()
        {
            int minLoad = this._communicator.GetLoad();
            string toReturn = null;
            foreach (string address in Configurator.Instance.LoadBalancerAddresses)
            {
                string answ = this.AskForLoadAndAddress(address);
                if (answ == null)
                {
                    continue;
                }

                string[] parts = answ.Split('$');
                int load = int.Parse(parts[0]);
                if (load < minLoad)
                {
                    toReturn = parts[1];
                    minLoad = load;
                }
            }

            return toReturn;
        }

        /// <summary>
        /// Starts answering thread.
        /// </summary>
        public void StartAnsweringThread()
        {
            if (!this.isAnswering)
            {
                this.AnsweringThread = new Thread(this.HandleLoadRequests);
                this.AnsweringThread.Name = "LB Ansewering Thread";
                this.AnsweringThread.IsBackground = true;

                this.AnsweringThread.Start();

                this.isAnswering = true;
            }
        }

        /// <summary>
        /// Starts asking thread.
        /// </summary>
        public void StartAskingThread()
        {
            if (!this.IsAsking)
            {
                this.AskingThread = new Thread(this.ProcessAskingForLoads);
                this.AskingThread.Name = "LB Asking Thread";
                this.AskingThread.IsBackground = true;

                this.AskingThread.Start();

                this.IsAsking = true;
            }
        }

        /// <summary>
        /// Stops answering thread.
        /// </summary>
        public void StopAnsweringThread()
        {
            if (this.isAnswering)
            {
                this.isAnswering = false;

                this.AnsweringThread.Join();
                this.AnsweringThread = null;
            }
        }

        /// <summary>
        /// Stops asking thread.
        /// </summary>
        public void StopAskingThread()
        {
            if (this.IsAsking)
            {
                this.IsAsking = false;

                this.AskingThread.Join();
                this.AskingThread = null;
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Answers load query.
        /// </summary>
        /// <param name="message">
        /// The message.
        /// </param>
        private void Answer(byte[] message)
        {
            string strMessage = Encoding.ASCII.GetString(message);
            if (strMessage.StartsWith("http://"))
            {
                strMessage = strMessage.Remove(0, 7);
            }

            string[] connectionInfo = strMessage.Split(':');
            IPAddress ip = IPAddress.Parse(connectionInfo[0]);
            int port = Convert.ToInt32(connectionInfo[1]);

            IPEndPoint endp = new IPEndPoint(ip, port);

            string load = this._communicator.GetLoad().ToString(CultureInfo.InvariantCulture);
            string strAnswer = load + '$' + Helpers.LoadBalanderHelper.GetIP4Address() + ':' + Configurator.Instance.Port.ToString(CultureInfo.InvariantCulture);
            byte[] answer = Encoding.ASCII.GetBytes(strAnswer);

            Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            socket.SendTo(answer, endp);
            socket.Close();
        }

        /// <summary>
        /// Ask for load and address.
        /// </summary>
        /// <param name="address">
        /// Remote server address.
        /// </param>
        /// <returns>
        /// Remote server address and load.
        /// </returns>
        private string AskForLoadAndAddress(string address)
        {
            address = address.Remove(0, 7).Replace("localhost", "127.0.0.1");
            string addr = address.Split(':')[0];
            int port = int.Parse(address.Split(':')[1]);
            IPEndPoint endP;

            var addresses = Dns.GetHostAddresses(addr);
            if (addresses.Length > 0)
            {
                endP = new IPEndPoint(addresses[0], port);
            }
            else
            {
                endP = new IPEndPoint(IPAddress.Parse(addr), port);
            }
            

            string strMessage = /*Configurator.Instance.Host.ToString()*/Helpers.LoadBalanderHelper.GetIP4Address() + ':' + Configurator.Instance.LoadBalancerAskingPort;
            byte[] message = Encoding.ASCII.GetBytes(strMessage);

            Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            socket.SendTo(message, endP);

            UdpClient listener = new UdpClient(Configurator.Instance.LoadBalancerAskingPort);
            listener.Client.ReceiveTimeout = 400;

            try
            {
                byte[] answer = listener.Receive(ref endP);
                listener.Close();
                return Encoding.ASCII.GetString(answer);
            }
            catch (SocketException)
            {
                listener.Close();
                return null;
            }
        }

        /// <summary>
        /// Handles load requests.
        /// </summary>
        private void HandleLoadRequests()
        {
            int port = Configurator.Instance.LoadBalancerAnsweringPort;
            
                UdpClient listener = new UdpClient(port);
                listener.Client.ReceiveTimeout = 400;

                while (this.isAnswering)
                {
                    try
                    {
                        IPEndPoint endPoint = null;
                        byte[] message = listener.Receive(ref endPoint);

                        if (message.Length > 0)
                        {
                            this.Answer(message);
                        }
                    }
                    catch (SocketException e)
                    {
                        if (e.SocketErrorCode != SocketError.TimedOut)
                        {
                            // only log not TimedOut exceptions, because TimedOut is OK by design
                            Logger.Instance.Log(string.Format("SocketException in UDP Listener, for port {0}", port));
                        }
                    }
                }

                listener.Close();
            
        }

        /// <summary>
        /// Process asking for loads.
        /// </summary>
        private void ProcessAskingForLoads()
        {
            int timeout = Configurator.Instance.LoadBalancerTimeOut;
            while (this.IsAsking)
            {
                this.LeastLoadedServer = this.GetServerWithMinLoad();
                Thread.Sleep(timeout);
            }
        }

        #endregion
    }
}